/*
 * Copyright (c) 2023 fred
 * Licensed under the Apache License,Version 2.0 (the "License")
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import { StringUtil } from '../internal/StringUtil'
import { QuirksMode } from '../nodes/QuirksMode'
import { DocumentType } from '../nodes/DocumentType'
import { HtmlTreeBuilder } from './HtmlTreeBuilder'
import { Token } from './Token'
import { TokeniserStates } from './TokeniserState'
import { Element } from '../nodes/Element'
import List from '@ohos.util.List'
import { Attributes } from '../nodes/Attributes'
import { Tag } from './Tag'
import { ParseSettings } from './ParseSettings'
import constants from '../util/Constants'
import { TokenStartTag } from './token/TokenStartTag'
import { TokenType } from './TokenType'
import { TokenCharacter } from './token/TokenCharacter'

export abstract class HtmlTreeBuilderState {

  abstract process(t: Token, tb: HtmlTreeBuilder): boolean

  abstract name():string

  static isWhitespace(t: Token): boolean {
    if (t.isCharacter()) {
      let data = t.asCharacter().getData()
      return StringUtil.isBlank(data)
    }
    return false
  }

  static handleRcData(startTag: TokenStartTag, tb: HtmlTreeBuilder) {
    tb.tokeniser.transition(TokeniserStates.Rcdata)
    tb.markInsertionMode()
    tb.transition(HtmlTreeBuilderStates.Text)
    tb.insert(startTag)
  }

  static handleRawtext(startTag: TokenStartTag, tb: HtmlTreeBuilder) {
    tb.tokeniser.transition(TokeniserStates.Rawtext)
    tb.markInsertionMode()
    tb.transition(HtmlTreeBuilderStates.Text)
    tb.insert(startTag)
  }
}


export const HtmlTreeBuilderStates: { [state: string]: HtmlTreeBuilderState } =
  {
    Initial: {
      name() :string{
        return "Initial"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (HtmlTreeBuilderState.isWhitespace(t)) {
          return true
        } else if (t.isComment()) {
          tb.insertComment(t.asComment())
        }
        else
          if (t.isDoctype()) {
            // todo: parse error check on expected doctypes
            // todo: quirk state check on doctype ids
            let d = t.asDoctype()
            let doctype = new DocumentType(tb.settings.normalizeTag(d.getName()), d.getPublicIdentifier(), d.getSystemIdentifier())
            doctype.setPubSysKey(d.getPubSysKey())
            tb.getDocument().appendChild(doctype)
            tb.onNodeInserted(doctype, t)
            if (d.isForceQuirks()) {
              tb.getDocument().setQuirksMode(QuirksMode.quirks)
            }
            tb.transition(HtmlTreeBuilderStates.BeforeHtml)
          } else {
          // todo: check not iframe srcdoc
          tb.transition(HtmlTreeBuilderStates.BeforeHtml)
          return tb.process(t) // re-process token
        }
        return true
      }
    },
    BeforeHtml: {
      name() :string{
        return "BeforeHtml"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (t.isDoctype()) {
          tb.errorState(this)
          return false
        }
        else
          if (t.isComment()) {
            tb.insertComment(t.asComment())
          } else if (HtmlTreeBuilderState.isWhitespace(t)) {
          // out of spec - include whitespace
          tb.insertCharacter(t.asCharacter())
        } else if (t.isStartTag() && t.asStartTag().normalName === 'html') {
          tb.insert(t.asStartTag())
          tb.transition(HtmlTreeBuilderStates.BeforeHead)
        } else if (t.isEndTag() &&
        (StringUtil.inSorted(t.asEndTag().getNormalName(), HtmlTreeBuilderStateConstants.BeforeHtmlToHead))) {
          return beforeHtmlAnythingElse(t, tb)
        } else if (t.isEndTag()) {
          tb.errorState(this)
        } else {
          return beforeHtmlAnythingElse(t, tb)
        }
        return true
      }
    },
    BeforeHead: {
      name() :string{
        return "BeforeHead"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (HtmlTreeBuilderState.isWhitespace(t)) {
          // out of spec - include whitespace
          tb.insertCharacter(t.asCharacter())
        }
        else
          if (t.isComment()) {
            tb.insertComment(t.asComment())
          } else if (t.isDoctype()) {
          tb.errorState(this)
          return false
        } else if (t.isStartTag() && t.asStartTag().getNormalName() === 'html') {
          // does not transition
          return HtmlTreeBuilderStates.InBody.process(t, tb)
        } else if (t.isStartTag() && t.asStartTag().getNormalName() === 'head') {
          let head = tb.insert(t.asStartTag())
          tb.setHeadElement(head)
          tb.transition(HtmlTreeBuilderStates.InHead)
        } else if (t.isEndTag() && StringUtil.inSorted(t.asEndTag().getNormalName(), HtmlTreeBuilderStateConstants.BeforeHtmlToHead)) {
          tb.processStartTag("head")
          return tb.process(t)
        } else if (t.isEndTag()) {
          tb.errorState(this)
          return false
        } else {
          tb.processStartTag('head')
          return tb.process(t)
        }
        return true
      }
    },
    InHead: {
      name() :string{
        return "InHead"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (HtmlTreeBuilderState.isWhitespace(t)) {
          // out of spec - include whitespace
          tb.insertCharacter(t.asCharacter())
          return true
        }
        switch (t.type) {
          case TokenType.Comment:
            tb.insertComment(t.asComment())
            break
          case TokenType.Doctype:
            tb.errorState(this)
            return false
          case TokenType.StartTag:
            let start = t.asStartTag()
            if (start === undefined || start === null) {
              return
            }
            let name = start.getNormalName()
            if (name === 'html') {
              return HtmlTreeBuilderStates.InBody.process(t, tb)
            } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InHeadEmpty)) {
              let el = tb.insertEmpty(start)
              // jsoup special: update base the first time it is seen
              if (name === 'base' && el.hasAttr('href')) {
                tb.maybeSetBaseUri(el)
              }
            } else if (name === 'meta') {
              tb.insertEmpty(start)
              // todo: charset switches
            } else if (name === 'title') {
              HtmlTreeBuilderState.handleRcData(start, tb)
            } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InHeadRaw)) {
              HtmlTreeBuilderState.handleRawtext(start, tb)
            } else if (name === 'noscript') {
              // else if noscript && scripting flag = true: rawtext (jsoup doesn't run script, to handle as noscript)
              tb.insert(start)
              tb.transition(HtmlTreeBuilderStates.InHeadNoscript)
            } else if (name === 'script') {
              // skips some script rules as won't execute them
              tb.tokeniser.transition(TokeniserStates.ScriptData)
              tb.markInsertionMode()
              tb.transition(HtmlTreeBuilderStates.Text)
              tb.insert(start)
            } else if (name === 'head') {
              tb.errorState(this)
              return false
            } else if (name === 'template') {
              tb.insert(start)
              tb.insertMarkerToFormattingElements()
              tb.setFramesetOk(false)
              tb.transition(HtmlTreeBuilderStates.InTemplate)
              tb.pushTemplateMode(HtmlTreeBuilderStates.InTemplate)
            } else {
              return inHeadAnythingElse(t, tb)
            }
            break
          case TokenType.EndTag:
            let end = t.asEndTag()
            name = end.getNormalName()
            if (name === 'head') {
              tb.pop()
              tb.transition(HtmlTreeBuilderStates.AfterHead)
            } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InHeadEnd)) {
              return inHeadAnythingElse(t, tb)
            } else if (name === 'template') {
              if (!tb.onStack(name)) {
                tb.errorState(this)
              } else {
                tb.generateImpliedEndTags(true)
                if (!(name === tb.currentElement().normalName())) {
                  tb.errorState(this)
                }
                tb.popStackToClose(name)
                tb.clearFormattingElementsToLastMarker()
                tb.popTemplateMode()
                tb.resetInsertionMode()
              }
            } else {
              tb.errorState(this)
              return false
            }
            break
          default:
            return inHeadAnythingElse(t, tb)
        }
        return true
      }
    },
    InHeadNoscript: {
      name() :string{
        return "InHeadNoscript"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (t.isDoctype()) {
          tb.errorState(this)
        }
        else
          if (t.isStartTag() && t.asStartTag().getNormalName() === 'html') {
            return tb.process(t, HtmlTreeBuilderStates.InBody)
          } else if (t.isEndTag() && t.asEndTag().getNormalName() === 'noscript') {
          tb.pop()
          tb.transition(HtmlTreeBuilderStates.InHead)
        } else if (HtmlTreeBuilderState.isWhitespace(t) || t.isComment() || (t.isStartTag() && StringUtil.inSorted(t.asStartTag().getNormalName(),
          HtmlTreeBuilderStateConstants.InHeadNoScriptHead))) {
          return tb.process(t, HtmlTreeBuilderStates.InHead)
        } else if (t.isEndTag() && t.asEndTag().getNormalName() === 'br') {
          tb.errorState(this)
          tb.insertCharacter(new TokenCharacter().setData(t.toString()))
          return true
        } else if ((t.isStartTag() && StringUtil.inSorted(t.asStartTag().getNormalName(), HtmlTreeBuilderStateConstants.InHeadNoscriptIgnore)) || t.isEndTag()) {
          tb.errorState(this)
          return false
        } else {
          tb.errorState(this)
          tb.insertCharacter(new TokenCharacter().setData(t.toString()))
          return true
        }
        return true
      }
    },
    AfterHead: {
      name() :string{
        return "AfterHead"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (HtmlTreeBuilderState.isWhitespace(t)) {
          tb.insertCharacter(t.asCharacter())
        }
        else
          if (t.isComment()) {
            tb.insertComment(t.asComment())
          } else if (t.isDoctype()) {
          tb.errorState(this)
        } else if (t.isStartTag()) {
          let startTag = t.asStartTag()
          let name = startTag.getNormalName()
          if (name === 'html') {
            return tb.process(t, HtmlTreeBuilderStates.InBody)
          } else if (name === 'body') {
            tb.insert(startTag)
            tb.setFramesetOk(false)
            tb.transition(HtmlTreeBuilderStates.InBody)
          } else if (name === 'frameset') {
            tb.insert(startTag)
            tb.transition(HtmlTreeBuilderStates.InFrameset)
          } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InBodyStartToHead)) {
            tb.errorState(this)
            let head = tb.getHeadElement()
            tb.push(head)
            tb.process(t, HtmlTreeBuilderStates.InHead)
            tb.removeFromStack(head)
          } else if (name === 'head') {
            tb.errorState(this)
            return false
          } else {
            tb.processStartTag("body")
            tb.setFramesetOk(true)
            return tb.process(t)
          }
        } else if (t.isEndTag()) {
          let name = t.asEndTag().getNormalName()
          if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.AfterHeadBody)) {
            tb.processStartTag("body")
            tb.setFramesetOk(true)
            return tb.process(t)
          } else if (name === 'template') {
            tb.process(t, HtmlTreeBuilderStates.InHead)
          } else {
            tb.errorState(this)
            return false
          }
        } else {
          tb.processStartTag("body")
          tb.setFramesetOk(true)
          return tb.process(t)
        }
        return true
      }
    },
    InBody: {
      name() :string{
        return "InBody"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        switch (t.type) {
          case
          TokenType
            .Character: {
            let c = t.asCharacter()
            if (c.getData() === constants.nullString) {
              // todo confirm that check
              tb.errorState(this)
              return false
            } else if (tb.getFramesetOk() && HtmlTreeBuilderState.isWhitespace(c)) {
              // don't check if whitespace if frames already closed
              tb.reconstructFormattingElements()
              tb.insertCharacter(c)
            } else {
              tb.reconstructFormattingElements()
              tb.insertCharacter(c)
              tb.setFramesetOk(false)
            }
            break
          }
          case
          TokenType
            .Comment: {
            tb.insertComment(t.asComment())
            break
          }
          case
          TokenType
            .Doctype: {
            tb.errorState(this)
            return false
          }
          case
          TokenType.StartTag:
            return inBodyStartTag(t, tb)
          case
          TokenType.EndTag:
            return inBodyEndTag(t, tb)
          case
          TokenType.EOF:
            if (tb.templateModeSize() > 0)
              return tb.process(t, HtmlTreeBuilderStates.InTemplate)
            if (tb.onStackNot(HtmlTreeBuilderStateConstants.InBodyEndOtherErrors))
              tb.errorState(this)
          // stop parsing
            break
        }
        return true
      }
    },
    Text: {
      name() :string{
        return "Text"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (t.isCharacter()) {
          tb.insertCharacter(t.asCharacter())
        }
        else
          if (t.isEOF()) {
            tb.errorState(this)
            // if current node is script: already started
            tb.pop()
            tb.transition(tb.originalState())
            return tb.process(t)
          } else if (t.isEndTag()) {
          // if: An end tag whose tag name is "script" -- scripting nesting level, if evaluating scripts
          tb.pop()
          tb.transition(tb.originalState())
        }
        return true
      }
    },
    InTable: {
      name() :string{
        return "InTable"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (t.isCharacter() && StringUtil.inSorted(tb.currentElement().normalName(),
          HtmlTreeBuilderStateConstants.InTableFoster)) {
          tb.resetPendingTableCharacters()
          tb.markInsertionMode()
          tb.transition(HtmlTreeBuilderStates.InTableText)
          return tb.process(t)
        } else if (t.isComment()) {
          tb.insertComment(t.asComment())
          return true;
          ;
        } else if (t.isDoctype()) {
          tb.errorState(this)
          return false
        } else if (t.isStartTag()) {
          let startTag = t.asStartTag()
          let name = startTag.getNormalName()
          if (name === 'caption') {
            tb.clearStackToTableContext()
            tb.insertMarkerToFormattingElements()
            tb.insert(startTag)
            tb.transition(HtmlTreeBuilderStates.InCaption)
          } else if (name === 'colgroup') {
            tb.clearStackToTableContext()
            tb.insert(startTag)
            tb.transition(HtmlTreeBuilderStates.InColumnGroup)
          } else if (name === 'col') {
            tb.clearStackToTableContext()
            tb.processStartTag('colgroup')
            return tb.process(t)
          } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InTableToBody)) {
            tb.clearStackToTableContext()
            tb.insert(startTag)
            tb.transition(HtmlTreeBuilderStates.InTableBody)
          } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InTableAddBody)) {
            tb.clearStackToTableContext()
            tb.processStartTag('tbody')
            return tb.process(t)
          } else if (name === 'table') {
            tb.errorState(this)
            if (!tb.inTableScope(name)) { // ignore it
              return false
            } else {
              tb.popStackToClose(name)
              if (!tb.resetInsertionMode()) {
                // not per spec - but haven't transitioned out of table. so try something else
                tb.insert(startTag)
                return true
              }
              return tb.process(t)
            }
          } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InTableToHead)) {
            return tb.process(t, HtmlTreeBuilderStates.InHead)
          } else if (name === 'input') {
            if (!(startTag.hasAttributes() && startTag.attributes.get('type').toLowerCase() === 'hidden'.toLowerCase())) {
              return inTableAnythingElse(t, tb);
            } else {
              tb.insertEmpty(startTag);
            }
          } else if (name === 'form') {
            tb.errorState(this)
            if (/*tb.getFormElement() != null ||*/ tb.onStack("template")) {
              return false
            } else {
              tb.insertForm(startTag, false, false) // not added to stack. can associate to template
            }
          } else {
            return inTableAnythingElse(t, tb)
          }
          return true // todo: check if should return processed http://www.whatwg.org/specs/web-apps/current-work/multipage/tree-construction.html#parsing-main-intable
        } else if (t.isEndTag()) {
          let endTag = t.asEndTag()
          let name = endTag.getNormalName()
          if (name === 'table') {
            if (!tb.inTableScope(name)) {
              tb.errorState(this)
              return false
            } else {
              tb.popStackToClose('table')
              tb.resetInsertionMode()
            }
          } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InTableEndErr)) {
            tb.errorState(this)
            return false
          } else if (name === 'template') {
            tb.process(t, HtmlTreeBuilderStates.InHead)
          } else {
            return inTableAnythingElse(t, tb)
          }
          return true // todo: as above todo
        } else if (t.isEOF()) {
          if (tb.currentElementIs("html")) {
            tb.errorState(this)
          }
          return true // stops parsing
        }
        return inTableAnythingElse(t, tb);
      }
    },
    InTableText: {
      name() :string{
        return "InTableText"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (t.type === TokenType.Character) {
          let c = t.asCharacter()
          if (c.getData() === constants.nullString) {
            tb.errorState(this)
            return false
          } else {
            tb.addPendingTableCharacters(c)
          }
        } else {
          if (tb.getPendingTableCharacters().length > 0) {
            for (let c of tb.getPendingTableCharacters()) {
              if (!HtmlTreeBuilderState.isWhitespace(c)) {
                // InTable anything else section:
                tb.errorState(this)
                if (StringUtil.inSorted(tb.currentElement().normalName(), HtmlTreeBuilderStateConstants.InTableFoster)) {
                  tb.setFosterInserts(true)
                  tb.process(c, HtmlTreeBuilderStates.InBody)
                  tb.setFosterInserts(false)
                } else {
                  tb.process(c, HtmlTreeBuilderStates.InBody)
                }
              } else {
                tb.insertCharacter(c)
              }
            }
            tb.resetPendingTableCharacters()
          }
          tb.transition(tb.originalState())
          return tb.process(t)
        }
        return true
      }
    },
    InCaption: {
      name() :string{
        return "InCaption"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (t.isEndTag() && t.asEndTag().getNormalName() === 'caption') {
          let endTag = t.asEndTag()
          let name = endTag.getNormalName()
          if (!tb.inTableScope(name)) {
            tb.errorState(this)
            return false
          } else {
            tb.generateImpliedEndTags()
            if (!tb.currentElementIs('caption')) {
              tb.errorState(this)
            }
            tb.popStackToClose("caption")
            tb.clearFormattingElementsToLastMarker()
            tb.transition(HtmlTreeBuilderStates.InTable)
          }
        } else if (t.isStartTag() && StringUtil.inSorted(t.asStartTag().getNormalName(), HtmlTreeBuilderStateConstants.InCellCol) ||
        t.isEndTag() && t.asEndTag().getNormalName() === 'table') {
          tb.errorState(this)
          let processed = tb.processEndTag('caption')
          if (processed) {
            return tb.process(t);
          }
        } else if (t.isEndTag() && StringUtil.inSorted(t.asEndTag().getNormalName(), HtmlTreeBuilderStateConstants.InCaptionIgnore)) {
          tb.errorState(this)
          return false
        } else {
          return tb.process(t, HtmlTreeBuilderStates.InBody)
        }
        return true
      }
    },
    InColumnGroup: {
      name() :string{
        return "InColumnGroup"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (HtmlTreeBuilderState.isWhitespace(t)) {
          tb.insertCharacter(t.asCharacter())
          return true
        }
        switch (t.type) {
          case TokenType.Comment:
            tb.insertComment(t.asComment())
            break
          case TokenType.Doctype:
            tb.errorState(this)
            break
          case TokenType.StartTag:
            let startTag = t.asStartTag()
            switch (startTag.getNormalName()) {
              case 'html':
                return tb.process(t, HtmlTreeBuilderStates.InBody)
              case 'col':
                tb.insertEmpty(startTag)
                break
              case 'template':
                tb.process(t, HtmlTreeBuilderStates.InHead)
                break
              default:
                return InColumnGroupAnythingElse(t, tb)
            }
            break
          case TokenType.EndTag:
            let endTag = t.asEndTag()
            let name = endTag.getNormalName()
            switch (name) {
              case 'colgroup':
                if (!tb.currentElementIs(name)) {
                  tb.errorState(this)
                  return false
                } else {
                  tb.pop()
                  tb.transition(HtmlTreeBuilderStates.InTable)
                }
                break
              case 'template':
                tb.process(t, HtmlTreeBuilderStates.InHead)
                break
              default:
                return InColumnGroupAnythingElse(t, tb)
            }
            break
          case TokenType.EOF:
            if (tb.currentElementIs('html'))
              return true // stop parsing; frag case
            else
              return InColumnGroupAnythingElse(t, tb)
          default:
            return InColumnGroupAnythingElse(t, tb)
        }
        return true
      }
    },
    InTableBody: {
      name() :string{
        return "InTableBody"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        switch (t.type) {
          case TokenType.StartTag:
            let startTag = t.asStartTag()
            let name = startTag.getNormalName()
            if (name === 'tr') {
              tb.clearStackToTableBodyContext()
              tb.insert(startTag)
              tb.transition(HtmlTreeBuilderStates.InRow)
            } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InCellNames)) {
              tb.errorState(this)
              tb.processStartTag('tr')
              return tb.process(startTag)
            } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InTableBodyExit)) {
              if (!(tb.inTableScope('tbody') || tb.inTableScope('thead') || tb.inScope('tfoot'))) {
                // frag case
                tb.errorState(this)
                return false
              }
              tb.clearStackToTableBodyContext()
              // tbody, tfoot, thead
              tb.processEndTag(tb.currentElement().normalName())
              return tb.process(t)
            } else {
              return tb.process(t, HtmlTreeBuilderStates.InTable)
            }
            break
          case TokenType.EndTag:
            let endTag = t.asEndTag()
            name = endTag.getNormalName()
            if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InTableEndIgnore)) {
              if (!tb.inTableScope(name)) {
                tb.errorState(this)
                return false
              } else {
                tb.clearStackToTableBodyContext()
                tb.pop()
                tb.transition(HtmlTreeBuilderStates.InTable)
              }
            } else if (name === 'table') {
              if (!(tb.inTableScope('tbody') || tb.inTableScope('thead') || tb.inScope('tfoot'))) {
                // frag case
                tb.errorState(this)
                return false
              }
              tb.clearStackToTableBodyContext()
              // tbody, tfoot, thead
              tb.processEndTag(tb.currentElement().normalName())
              return tb.process(t)
            } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InTableBodyEndIgnore)) {
              tb.errorState(this)
              return false
            } else {
              return tb.process(t, HtmlTreeBuilderStates.InTable)
            }
            break
          default:
            return tb.process(t, HtmlTreeBuilderStates.InTable)
        }
        return true;
      }
    },
    InRow: {
      name() :string{
        return "InRow"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (t.isStartTag()) {
          let startTag = t.asStartTag()
          let name = startTag.getNormalName()
          if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InCellNames)) {
            tb.clearStackToTableRowContext()
            tb.insert(startTag)
            tb.transition(HtmlTreeBuilderStates.InCell)
            tb.insertMarkerToFormattingElements()
          } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InRowMissing)) {
            let processed = tb.processEndTag('tr')
            if (processed)
              return tb.process(t)
            else {
              return false
            }
          } else {
            return tb.process(t, HtmlTreeBuilderStates.InTable)
          }
        } else if (t.isEndTag()) {
          let endTag = t.asEndTag()
          let name = endTag.getNormalName()
          if (name === 'tr') {
            if (!tb.inTableScope(name)) {
              tb.errorState(this) // frag
              return false
            }
            tb.clearStackToTableRowContext()
            tb.pop() // tr
            tb.transition(HtmlTreeBuilderStates.InTableBody)
          } else if (name === 'table') {
            let processed = tb.processEndTag('tr')
            if (processed)
              return tb.process(t)
            else {
              return false
            }
          } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InTableToBody)) {
            if (!tb.inTableScope(name) || !tb.inTableScope('tr')) {
              tb.errorState(this)
              return false
            }
            tb.clearStackToTableRowContext()
            tb.pop() // tr
            tb.transition(HtmlTreeBuilderStates.InTableBody)
          } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InRowIgnore)) {
            tb.errorState(this)
            return false
          } else {
            return tb.process(t, HtmlTreeBuilderStates.InTable)
          }
        } else {
          return tb.process(t, HtmlTreeBuilderStates.InTable)
        }
        return true
      }
    },
    InCell: {
      name() :string{
        return "InCell"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (t.isEndTag()) {
          let endTag = t.asEndTag()
          let name = endTag.getNormalName()
          if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InCellNames)) {
            if (!tb.inTableScope(name)) {
              tb.errorState(this)
              // might not be in scope if empty: <td /> and processing fake end tag
              tb.transition(HtmlTreeBuilderStates.InRow)
              return false
            }
            tb.generateImpliedEndTags()
            if (!tb.currentElementIs(name)) {
              tb.errorState(this)
            }
            tb.popStackToClose(name)
            tb.clearFormattingElementsToLastMarker()
            tb.transition(HtmlTreeBuilderStates.InRow)
          } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InCellBody)) {
            tb.errorState(this)
            return false
          } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InCellTable)) {
            if (!tb.inTableScope(name)) {
              tb.errorState(this)
              return false
            }
            if (tb.inTableScope('td')) {
              tb.processEndTag('td')
            } else {
              // only here if th or td in scope
              tb.processEndTag('th')
            }
            return tb.process(t)
          } else {
            return tb.process(t, HtmlTreeBuilderStates.InBody)
          }
        } else if (t.isStartTag() &&
        StringUtil.inSorted(t.asStartTag().getNormalName(), HtmlTreeBuilderStateConstants.InCellCol)) {
          if (!(tb.inTableScope('td') || tb.inTableScope('th'))) {
            tb.errorState(this)
            return false
          }
          if (tb.inTableScope('td')) {
            tb.processEndTag('td')
          } else {
            // only here if th or td in scope
            tb.processEndTag('th')
          }
          return tb.process(t)
        } else {
          return tb.process(t, HtmlTreeBuilderStates.InBody)
        }
        return true
      }
    },
    InSelect: {
      name() :string{
        return "InSelect"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        switch (t.type) {
          case TokenType.Character:
            let c = t.asCharacter()
            if (c.getData() === constants.nullString) {
              tb.errorState(this)
              return false
            } else {
              tb.insertCharacter(c)
            }
            break
          case TokenType.Comment:
            tb.insertComment(t.asComment())
            break;
          case TokenType.Doctype:
            tb.errorState(this)
            return false
          case TokenType.StartTag:
            let start = t.asStartTag()
            let name = start.getNormalName()
            if (name === 'html') {
              return tb.process(start, HtmlTreeBuilderStates.InBody)
            } else if (name === 'option') {
              if (tb.currentElementIs('option')) {
                tb.processEndTag('option')
              }
              tb.insert(start)
            } else if (name === 'optgroup') {
              if (tb.currentElementIs('option')) {
                // pop option and flow to pop optgroup
                tb.processEndTag('option')
              }
              if (tb.currentElementIs('optgroup')) {
                tb.processEndTag('optgroup')
              }
              tb.insert(start)
            } else if (name === 'select') {
              tb.errorState(this)
              return tb.processEndTag('select')
            } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InSelectEnd)) {
              tb.errorState(this);
              if (!tb.inSelectScope('select')) {
                return false; // frag
              }
              tb.processEndTag('select')
              return tb.process(start);
            } else if (name === 'script' || name === 'template') {
              return tb.process(t, HtmlTreeBuilderStates.InHead)
            } else {
              tb.errorState(this)
              return false
            }
            break;
          case TokenType.EndTag:
            let end = t.asEndTag()
            name = end.getNormalName()
            switch (name) {
              case 'optgroup':
                if (tb.currentElementIs('option') && tb.aboveOnStack(tb.currentElement()) != null &&
                tb.aboveOnStack(tb.currentElement()).normalName() === 'optgroup') {
                  tb.processEndTag('option')
                }
                if (tb.currentElementIs('optgroup')) {
                  tb.pop()
                } else {
                  tb.errorState(this)
                }
                break
              case 'option':
                if (tb.currentElementIs('option')) {
                  tb.pop()
                } else {
                  tb.errorState(this)
                }
                break
              case 'select':
                if (!tb.inSelectScope(name)) {
                  tb.errorState(this)
                  return false
                } else {
                  tb.popStackToClose(name)
                  tb.resetInsertionMode()
                }
                break
              case 'template':
                return tb.process(t, HtmlTreeBuilderStates.InHead)
              default:
                tb.errorState(this)
                return false
            }
            break;
          case TokenType.EOF:
            if (!tb.currentElementIs('html'))
              tb.errorState(this)
            break;
          default:
            tb.errorState(this)
            return false
        }
        return true;
      }
    },
    InSelectInTable: {
      name() :string{
        return "InSelectInTable"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (t.isStartTag() && StringUtil.inSorted(t.asStartTag().getNormalName(), HtmlTreeBuilderStateConstants.InSelectTableEnd)) {
          tb.errorState(this)
          tb.popStackToClose('select')
          tb.resetInsertionMode()
          return tb.process(t)
        } else if (t.isEndTag() && StringUtil.inSorted(t.asEndTag().getNormalName(), HtmlTreeBuilderStateConstants.InSelectTableEnd)) {
          tb.errorState(this)
          if (tb.inTableScope(t.asEndTag().getNormalName())) {
            tb.popStackToClose('select')
            tb.resetInsertionMode()
            return (tb.process(t))
          } else {
            return false
          }
        } else {
          return tb.process(t, HtmlTreeBuilderStates.InSelect)
        }
      }
    },
    InTemplate: {
      name() :string{
        return "InTemplate"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        let name: string
        switch (t.type) {
          case TokenType.Character:
          case TokenType.Comment:
          case TokenType.Doctype:
            tb.process(t, HtmlTreeBuilderStates.InBody)
            break
          case TokenType.StartTag:
            name = t.asStartTag().getNormalName()
            if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InTemplateToHead)) {
              tb.process(t, HtmlTreeBuilderStates.InHead)
            } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InTemplateToTable)) {
              tb.popTemplateMode()
              tb.pushTemplateMode(HtmlTreeBuilderStates.InTable)
              tb.transition(HtmlTreeBuilderStates.InTable)
              return tb.process(t)
            } else if (name === 'col') {
              tb.popTemplateMode()
              tb.pushTemplateMode(HtmlTreeBuilderStates.InColumnGroup)
              tb.transition(HtmlTreeBuilderStates.InColumnGroup)
              return tb.process(t)
            } else if (name === 'tr') {
              tb.popTemplateMode()
              tb.pushTemplateMode(HtmlTreeBuilderStates.InTableBody)
              tb.transition(HtmlTreeBuilderStates.InTableBody)
              return tb.process(t)
            } else if (name === 'td' || name === 'th') {
              tb.popTemplateMode()
              tb.pushTemplateMode(HtmlTreeBuilderStates.InRow)
              tb.transition(HtmlTreeBuilderStates.InRow)
              return tb.process(t)
            } else {
              tb.popTemplateMode()
              tb.pushTemplateMode(HtmlTreeBuilderStates.InBody)
              tb.transition(HtmlTreeBuilderStates.InBody)
              return tb.process(t)
            }
            break
          case TokenType.EndTag:
            name = t.asEndTag().getNormalName()
            if (name === 'template') {
              tb.process(t, HtmlTreeBuilderStates.InHead)
            } else {
              tb.errorState(this)
              return false
            }
            break
          case TokenType.EOF:
            if (!tb.onStack('template')) { // stop parsing
              return true
            }
            tb.errorState(this)
            tb.popStackToClose('template')
            tb.clearFormattingElementsToLastMarker()
            tb.popTemplateMode()
            tb.resetInsertionMode()
          // spec deviation - if we did not break out of Template, stop processing, and don't worry about cleaning up ultra-deep template stacks
          // limited depth because this can recurse and will blow stack if too deep
            if (tb.state() != HtmlTreeBuilderStates.InTemplate && tb.templateModeSize() < 12) {
              return tb.process(t)
            } else {
              return true
            }
        }
        return true
      }
    },
    AfterBody: {
      name() :string{
        return "AfterBody"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (HtmlTreeBuilderState.isWhitespace(t)) {
          // spec deviation - currently body is still on stack, but we want this to go to the html node
          let html = tb.getFromStack('html')
          if (html != null) {
            tb.insertCharacter(t.asCharacter(), html)
          } else {
            // will get into body
            tb.process(t, HtmlTreeBuilderStates.InBody)
          }
        } else if (t.isComment()) {
          tb.insertComment(t.asComment()) // into html node
        } else if (t.isDoctype()) {
          tb.errorState(this)
          return false
        } else if (t.isStartTag() && t.asStartTag().getNormalName() === 'html') {
          return tb.process(t, HtmlTreeBuilderStates.InBody)
        } else if (t.isEndTag() && t.asEndTag().getNormalName() === 'html') {
          if (tb.isFragmentParsing()) {
            tb.errorState(this)
            return false
          } else {
            tb.transition(HtmlTreeBuilderStates.AfterAfterBody)
          }
        } else if (t.isEOF()) {
          // chillax! we're done
        } else {
          tb.errorState(this)
          tb.resetBody()
          return tb.process(t)
        }
        return true
      }
    },
    InFrameset: {
      name() :string{
        return "InFrameset"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (HtmlTreeBuilderState.isWhitespace(t)) {
          tb.insertCharacter(t.asCharacter())
        } else if (t.isComment()) {
          tb.insertComment(t.asComment())
        } else if (t.isDoctype()) {
          tb.errorState(this)
          return false
        } else if (t.isStartTag()) {
          let start = t.asStartTag();
          switch (start.getNormalName()) {
            case 'html':
              return tb.process(start, HtmlTreeBuilderStates.InBody)
            case 'frameset':
              tb.insert(start)
              break;
            case 'frame':
              tb.insertEmpty(start)
              break;
            case 'noframes':
              return tb.process(start, HtmlTreeBuilderStates.InHead)
            default:
              tb.errorState(this)
              return false
          }
        } else if (t.isEndTag() && t.asEndTag().getNormalName() === 'frameset') {
          if (tb.currentElementIs('html')) { // frag
            tb.errorState(this)
            return false
          } else {
            tb.pop()
            if (!tb.isFragmentParsing() && !tb.currentElementIs("frameset")) {
              tb.transition(HtmlTreeBuilderStates.AfterFrameset)
            }
          }
        } else if (t.isEOF()) {
          if (!tb.currentElementIs("html")) {
            tb.errorState(this)
            return true
          }
        } else {
          tb.errorState(this)
          return false
        }
        return true
      }
    },
    AfterFrameset: {
      name() :string{
        return "AfterFrameset"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (HtmlTreeBuilderState.isWhitespace(t)) {
          tb.insertCharacter(t.asCharacter())
        } else if (t.isComment()) {
          tb.insertComment(t.asComment())
        } else if (t.isDoctype()) {
          tb.errorState(this)
          return false
        } else if (t.isStartTag() && t.asStartTag().getNormalName() === 'html') {
          return tb.process(t, HtmlTreeBuilderStates.InBody)
        } else if (t.isEndTag() && t.asEndTag().getNormalName() === 'html') {
          tb.transition(HtmlTreeBuilderStates.AfterAfterFrameset)
        } else if (t.isStartTag() && t.asStartTag().getNormalName() === 'noframes') {
          return tb.process(t, HtmlTreeBuilderStates.InHead)
        } else if (t.isEOF()) {
          // cool your heels, we're complete
        } else {
          tb.errorState(this)
          return false
        }
        return true
      }
    },
    AfterAfterBody: {
      name() :string{
        return "AfterAfterBody"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (t.isComment()) {
          tb.insertComment(t.asComment())
        } else if (t.isDoctype() || (t.isStartTag() && t.asStartTag().getNormalName() === 'html')) {
          return tb.process(t, HtmlTreeBuilderStates.InBody)
        } else if (HtmlTreeBuilderState.isWhitespace(t)) {
          // spec deviation - body and html still on stack, but want this space to go after </html>
          let doc = tb.getDocument()
          tb.insertCharacter(t.asCharacter(), doc)
        } else if (t.isEOF()) {
          // nice work chuck
        } else {
          tb.errorState(this)
          tb.resetBody()
          return tb.process(t)
        }
        return true
      }
    },
    AfterAfterFrameset: {
      name() :string{
        return "AfterAfterFrameset"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        if (t.isComment()) {
          tb.insertComment(t.asComment())
        } else if (t.isDoctype() || HtmlTreeBuilderState.isWhitespace(t) || (t.isStartTag() && t.asStartTag().getNormalName() === 'html')) {
          return tb.process(t, HtmlTreeBuilderStates.InBody)
        } else if (t.isEOF()) {
          // nice work chuck
        } else if (t.isStartTag() && t.asStartTag().getNormalName() === 'noframes') {
          return tb.process(t, HtmlTreeBuilderStates.InHead)
        } else {
          tb.errorState(this)
          return false
        }
        return true
      }
    },
    ForeignContent: {
      name() :string{
        return "ForeignContent"
      },
      process(t: Token, tb: HtmlTreeBuilder): boolean {
        return true
        // todo: implement. Also how do we get here?
      }
    }
  }

export class HtmlTreeBuilderStateConstants {
  static readonly InHeadEmpty = ["base", "basefont", "bgsound", "command", "link"]
  static readonly InHeadRaw = ["noframes", "style"]
  static readonly InHeadEnd = ["body", "br", "html"]
  static readonly AfterHeadBody = ["body", "br", "html"]
  static readonly BeforeHtmlToHead = ["body", "br", "head", "html"]
  static readonly InHeadNoScriptHead = ["basefont", "bgsound", "link", "meta", "noframes", "style"]
  static readonly InBodyStartToHead = ["base", "basefont", "bgsound", "command", "link", "meta", "noframes", "script", "style", "template", "title"]
  static readonly InBodyStartPClosers = ["address", "article", "aside", "blockquote", "center", "details", "dir", "div", "dl",
    "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "menu", "nav", "ol",
    "p", "section", "summary", "ul"]
  static readonly Headings = ["h1", "h2", "h3", "h4", "h5", "h6"]
  static readonly InBodyStartLiBreakers = ["address", "div", "p"]
  static readonly DdDt = ["dd", "dt"]
  static readonly InBodyStartApplets = ["applet", "marquee", "object"]
  static readonly InBodyStartMedia = ["param", "source", "track"]
  static readonly InBodyStartInputAttribs = ["action", "name", "prompt"]
  static readonly InBodyStartDrop = ["caption", "col", "colgroup", "frame", "head", "tbody", "td", "tfoot", "th", "thead", "tr"]
  static readonly InBodyEndClosers = ["address", "article", "aside", "blockquote", "button", "center", "details", "dir", "div",
    "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "listing", "menu",
    "nav", "ol", "pre", "section", "summary", "ul"]
  static readonly InBodyEndOtherErrors = ["body", "dd", "dt", "html", "li", "optgroup", "option", "p", "rb", "rp", "rt", "rtc", "tbody", "td", "tfoot", "th", "thead", "tr"]
  static readonly InBodyEndAdoptionFormatters = ["a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u"]
  static readonly InBodyEndTableFosters = ["table", "tbody", "tfoot", "thead", "tr"]
  static readonly InTableToBody = ["tbody", "tfoot", "thead"]
  static readonly InTableAddBody = ["td", "th", "tr"]
  static readonly InTableToHead = ["script", "style", "template"]
  static readonly InCellNames = ["td", "th"]
  static readonly InCellBody = ["body", "caption", "col", "colgroup", "html"]
  static readonly InCellTable = ["table", "tbody", "tfoot", "thead", "tr"]
  static readonly InCellCol = ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"]
  static readonly InTableEndErr = ["body", "caption", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr"]
  static readonly InTableFoster = ["table", "tbody", "tfoot", "thead", "tr"]
  static readonly InTableBodyExit = ["caption", "col", "colgroup", "tbody", "tfoot", "thead"]
  static readonly InTableBodyEndIgnore = ["body", "caption", "col", "colgroup", "html", "td", "th", "tr"]
  static readonly InRowMissing = ["caption", "col", "colgroup", "tbody", "tfoot", "thead", "tr"]
  static readonly InRowIgnore = ["body", "caption", "col", "colgroup", "html", "td", "th"]
  static readonly InSelectEnd = ["input", "keygen", "textarea"]
  static readonly InSelectTableEnd = ["caption", "table", "tbody", "td", "tfoot", "th", "thead", "tr"]
  static readonly InTableEndIgnore = ["tbody", "tfoot", "thead"]
  static readonly InHeadNoscriptIgnore = ["head", "noscript"]
  static readonly InCaptionIgnore = ["body", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr"]
  static readonly InTemplateToHead = ["base", "basefont", "bgsound", "link", "meta", "noframes", "script", "style", "template", "title"]
  static readonly InTemplateToTable = ["caption", "colgroup", "tbody", "tfoot", "thead"]
}


function beforeHtmlAnythingElse(t: Token, tb: HtmlTreeBuilder): boolean {
  tb.insertStartTag('html')
  tb.transition(HtmlTreeBuilderStates.BeforeHead)
  return tb.process(t)
}

function inHeadAnythingElse(t: Token, tb: HtmlTreeBuilder): boolean {
  tb.processEndTag("head")
  return tb.process(t)
}

function inBodyStartTag(t: Token, tb: HtmlTreeBuilder): boolean {
  let startTag = t.asStartTag()
  let name = startTag.getNormalName()
  let stack: List<Element>
  let el: Element

  switch (name) {
    case 'a':
      if (tb.getActiveFormattingElement("a") != null) {
        tb.errorState(this)
        tb.processEndTag("a")

        // still on stack?
        let remainingA = tb.getFromStack("a")
        if (remainingA != null) {
          tb.removeFromActiveFormattingElements(remainingA)
          tb.removeFromStack(remainingA)
        }
      }
      tb.reconstructFormattingElements()
      el = tb.insert(startTag)
      tb.pushActiveFormattingElements(el)
      break
    case 'span':
    // same as final else, but short circuits lots of checks
      tb.reconstructFormattingElements()
      tb.insert(startTag)
      break
    case 'li':
      tb.setFramesetOk(false)
      stack = tb.getStack()
      for (let i = stack.length - 1; i > 0; i--) {
        el = stack.get(i)
        if (el.normalName() === 'li') {
          tb.processEndTag("li")
          break
        }
        if (tb.isSpecial(el) && !StringUtil.inSorted(el.normalName(), HtmlTreeBuilderStateConstants.InBodyStartLiBreakers))
          break
      }

      if (tb.inButtonScope("p")) {
        tb.processEndTag("p")
      }
      tb.insert(startTag)
      break
    case 'html':
      tb.errorState(this)
      if (tb.onStack("template")) {
        return false // ignore
      }
    // otherwise, merge attributes onto real html (if present)
      stack = tb.getStack()
      if (stack.length > 0) {
        let html = tb.getStack().get(0)
        if (startTag.hasAttributes()) {
          for (let attribute of startTag.attributes) {
            if (!html.hasAttr(attribute.getKey()))
              html.attributes().putAttributes(attribute)
          }
        }
      }
      break
    case 'body':
      tb.errorState(this)
      stack = tb.getStack()
      if (stack.length == 1 || (stack.length > 2 && stack.get(1).normalName() !== 'body') || tb.onStack("template")) {
        // only in fragment case
        return false
      } else {
        tb.setFramesetOk(false)
        // will be on stack if this is a nested body. won't be if closed (which is a variance from spec, which leaves it on)
        let body: Element
        if (startTag.hasAttributes() && (body = tb.getFromStack("body")) != null) {
          // we only ever put one body on stack
          for (let attribute of startTag.attributes) {
            if (!body.hasAttr(attribute.getKey()))
              body.attributes().putAttributes(attribute)
          }
        }
      }
      break
    case 'frameset':
      tb.errorState(this)
      stack = tb.getStack()
      if (stack.length == 1 || (stack.length > 2 && stack.get(1).normalName() !== 'body')) {
        // only in fragment case
        return false
      } else if (!tb.getFramesetOk()) {
        return false // ignore frameset
      } else {
        let second = stack.get(1)
        if (second.parent() != null)
          second.remove()
        // pop up to html element
        while (stack.length > 1) {
          stack.removeByIndex(stack.length - 1)
        }
        tb.insert(startTag)
        tb.transition(HtmlTreeBuilderStates.InFrameset)
      }
      break
    case 'form':
      if (/*tb.getFormElement() != null &&*/ !tb.onStack('template')) {
        tb.errorState(this)
        return false
      }
      if (tb.inButtonScope('p')) {
        tb.closeElement('p')
      }
      tb.insertForm(startTag, true, true) // won't associate to any template
      break
    case 'plaintext':
      if (tb.inButtonScope('p')) {
        tb.processEndTag('p')
      }
      tb.insert(startTag)
      tb.tokeniser.transition(TokeniserStates.PLAINTEXT) // once in, never gets out
      break
    case 'button':
      if (tb.inButtonScope('button')) {
        // close and reprocess
        tb.errorState(this)
        tb.processEndTag('button')
        tb.process(startTag)
      } else {
        tb.reconstructFormattingElements()
        tb.insert(startTag)
        tb.setFramesetOk(false)
      }
      break
    case 'nobr':
      tb.reconstructFormattingElements()
      if (tb.inScope('nobr')) {
        tb.errorState(this)
        tb.processEndTag('nobr')
        tb.reconstructFormattingElements()
      }
      el = tb.insert(startTag)
      tb.pushActiveFormattingElements(el)
      break
    case 'table':
      if (tb.getDocument().getQuirksMode() != QuirksMode.quirks && tb.inButtonScope('p')) {
        tb.processEndTag('p')
      }
      tb.insert(startTag)
      tb.setFramesetOk(false)
      tb.transition(HtmlTreeBuilderStates.InTable)
      break
    case 'input':
      tb.reconstructFormattingElements()
      el = tb.insertEmpty(startTag)
      if (el.getAttr('type').toLowerCase() !== 'hidden'.toLowerCase())
        tb.setFramesetOk(false)
      break
    case 'hr':
      if (tb.inButtonScope('p')) {
        tb.processEndTag('p')
      }
      tb.insertEmpty(startTag)
      tb.setFramesetOk(false)
      break
    case 'image':
      if (tb.getFromStack('svg') == null)
        return tb.process(startTag.setName('img')) // change <image> to <img>, unless in svg
      else
        tb.insert(startTag)
      break
    case 'isindex':
    // how much do we care about the early 90s?
      tb.errorState(this)
      /*if (tb.getFormElement() != null) {
        return false
      }*/
      tb.processStartTag('form')
      if (startTag.hasAttribute('action')) {
        // let form = tb.getFormElement()
        // if (form != null && startTag.hasAttribute('action')) {
        //   let action = startTag.attributes.get('action')
        //   // always LC, so don't need to scan up for ownerdoc
        //   form.attributes().put('action', action)
        // }
      }
      tb.processStartTag('hr')
      tb.processStartTag('label')
    // hope you like english.
      let prompt = startTag.hasAttribute('prompt') ?
      startTag.attributes.get('prompt') :
        "This is a searchable index. Enter search keywords: "

      tb.process(new TokenCharacter().setData(prompt))

    // input
      let inputAttribs = new Attributes()
      if (startTag.hasAttributes()) {
        for (let attr of startTag.attributes) {
          if (!StringUtil.inSorted(attr.getKey(), HtmlTreeBuilderStateConstants.InBodyStartInputAttribs)) {
            inputAttribs.putAttributes(attr)
          }
        }
      }
      inputAttribs.put('name', 'isindex')
      tb.processStartTag('input', inputAttribs)
      tb.processEndTag('label')
      tb.processStartTag('hr')
      tb.processEndTag('form')
      break
    case 'textarea':
      tb.insert(startTag)
      if (!startTag.isSelfClosing()) {
        tb.tokeniser.transition(TokeniserStates.Rcdata)
        tb.markInsertionMode()
        tb.setFramesetOk(false)
        tb.transition(HtmlTreeBuilderStates.Text)
      }
      break
    case 'xmp':
      if (tb.inButtonScope('p')) {
        tb.processEndTag('p')
      }
      tb.reconstructFormattingElements()
      tb.setFramesetOk(false)
      HtmlTreeBuilderState.handleRawtext(startTag, tb)
      break
    case 'iframe':
      tb.setFramesetOk(false)
      HtmlTreeBuilderState.handleRawtext(startTag, tb)
      break
    case 'noembed':
    // also handle noscript if script enabled
      HtmlTreeBuilderState.handleRawtext(startTag, tb)
      break
    case 'select':
      tb.reconstructFormattingElements()
      tb.insert(startTag)
      tb.setFramesetOk(false)
    // don't change states if not added to the stack
      if (startTag.selfClosing) {
        break
      }
      let state = tb.state()
      if (state === HtmlTreeBuilderStates.InTable || state === HtmlTreeBuilderStates.InCaption ||
      state === HtmlTreeBuilderStates.InTableBody || state === HtmlTreeBuilderStates.InRow ||
      state === HtmlTreeBuilderStates.InCell) {
        tb.transition(HtmlTreeBuilderStates.InSelectInTable)
      } else {
        tb.transition(HtmlTreeBuilderStates.InSelect)
      }
      break
    case 'math':
      tb.reconstructFormattingElements()
    // todo: handle A start tag whose tag name is "math" (i.e. foreign, mathml)
      tb.insert(startTag)
      break
    case 'svg':
      tb.reconstructFormattingElements()
    // todo: handle A start tag whose tag name is "svg" (xlink, svg)
      tb.insert(startTag)
      break
  // static final String[] Headings = new String[]{"h1", "h2", "h3", "h4", "h5", "h6"}
    case 'h1':
    case 'h2':
    case 'h3':
    case 'h4':
    case 'h5':
    case 'h6':
      if (tb.inButtonScope('p')) {
        tb.processEndTag('p')
      }
      if (StringUtil.inSorted(tb.currentElement().normalName(), HtmlTreeBuilderStateConstants.Headings)) {
        tb.errorState(this)
        tb.pop()
      }
      tb.insert(startTag)
      break
  // static final String[] InBodyStartPreListing = new String[]{"listing", "pre"}
    case 'pre':
    case 'listing':
      if (tb.inButtonScope('p')) {
        tb.processEndTag('p')
      }
      tb.insert(startTag)
      tb.reader.matchConsume("\n") // ignore LF if next token
      tb.setFramesetOk(false)
      break
  // static final String[] DdDt = new String[]{"dd", "dt"}
    case 'dd':
    case 'dt':
      tb.setFramesetOk(false)
      stack = tb.getStack()
      let bottom = stack.length - 1
      let upper = bottom >= 24 ? bottom - 24 : 0
      for (let i = bottom; i >= upper; i--) {
        el = stack.get(i)
        if (StringUtil.inSorted(el.normalName(), HtmlTreeBuilderStateConstants.DdDt)) {
          tb.processEndTag(el.normalName())
          break
        }
        if (tb.isSpecial(el) && !StringUtil.inSorted(el.normalName(), HtmlTreeBuilderStateConstants.InBodyStartLiBreakers)) {
          break
        }
      }

      if (tb.inButtonScope('p')) {
        tb.processEndTag('p')
      }
      tb.insert(startTag)
      break
    case 'optgroup':
    case 'option':
      if (tb.currentElementIs('option'))
        tb.processEndTag('option')
      tb.reconstructFormattingElements()
      tb.insert(startTag)
      break
    case 'rb':
    case 'rtc':
      if (tb.inScope('ruby')) {
        tb.generateImpliedEndTags()
        if (!tb.currentElementIs('ruby'))
          tb.errorState(this)
      }
      tb.insert(startTag)
      break
    case 'rp':
    case 'rt':
      if (tb.inScope('ruby')) {
        tb.generateImpliedEndTags('rtc')
        if (!tb.currentElementIs('rtc') && !tb.currentElementIs('ruby'))
          tb.errorState(this)
      }
      tb.insert(startTag)
      break
  // InBodyStartEmptyFormatters:
    case 'area':
    case 'br':
    case 'embed':
    case 'img':
    case 'keygen':
    case 'wbr':
      tb.reconstructFormattingElements()
      tb.insertEmpty(startTag)
      tb.setFramesetOk(false)
      break
  // Formatters:
    case 'b':
    case 'big':
    case 'code':
    case 'em':
    case 'font':
    case 'i':
    case 's':
    case 'small':
    case 'strike':
    case 'strong':
    case 'tt':
    case 'u':
      tb.reconstructFormattingElements()
      el = tb.insert(startTag)
      tb.pushActiveFormattingElements(el)
      break
    default
    :
    // todo - bring scan groups in if desired
      if (!Tag.isKnownTag(name)) { // no special rules for custom tags
        tb.insert(startTag)
      } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InBodyStartPClosers)) {
        if (tb.inButtonScope('p')) {
          tb.processEndTag('p')
        }
        tb.insert(startTag)
      } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InBodyStartToHead)) {
        return tb.process(t, HtmlTreeBuilderStates.InHead)
      } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InBodyStartApplets)) {
        tb.reconstructFormattingElements()
        tb.insert(startTag)
        tb.insertMarkerToFormattingElements()
        tb.setFramesetOk(false)
      } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InBodyStartMedia)) {
        tb.insertEmpty(startTag)
      } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InBodyStartDrop)) {
        tb.errorState(this)
        return false
      } else {
        tb.reconstructFormattingElements()
        tb.insert(startTag)
      }
  }
  return true
}

function inBodyEndTag(t: Token, tb: HtmlTreeBuilder): boolean {
  let endTag = t.asEndTag()
  let name = endTag.getNormalName()
  switch (name) {
    case 'template':
      tb.process(t, HtmlTreeBuilderStates.InHead)
      break
    case 'sarcasm': // *sigh*
    case 'span':
    // same as final fall through, but saves short circuit
      return anyOtherEndTag(t, tb);
    case 'li':
      if (!tb.inListItemScope(name)) {
        tb.errorState(this)
        return false
      } else {
        tb.generateImpliedEndTags(name)
        if (!tb.currentElementIs(name)) {
          tb.errorState(this)
        }
        tb.popStackToClose(name)
      }
      break;
    case 'body':
      if (!tb.inScope('body')) {
        tb.errorState(this);
        return false;
      } else {
        if (tb.onStackNot(HtmlTreeBuilderStateConstants.InBodyEndOtherErrors))
          tb.errorState(this);
        tb.transition(HtmlTreeBuilderStates.AfterBody);
      }
      break;
    case 'html':
      if (!tb.onStack('body')) {
        tb.errorState(this)
        return false
      } else {
        if (tb.onStackNot(HtmlTreeBuilderStateConstants.InBodyEndOtherErrors)) {
          tb.errorState(this)
        }
        tb.transition(HtmlTreeBuilderStates.AfterBody)
        return tb.process(t)
      }

    case 'form':
      if (!tb.onStack('template')) {
        // let currentForm = tb.getFormElement()
        // tb.setFormElement(null)
        // if (currentForm == null || !tb.inScope(name)) {
        //   tb.errorState(this)
        //   return false
        // }
        tb.generateImpliedEndTags()
        if (!tb.currentElementIs(name)) {
          tb.errorState(this)
        }
        // remove currentForm from stack. will shift anything under up.
        // tb.removeFromStack(currentForm)
      } else { // template on stack
        if (!tb.inScope(name)) {
          tb.errorState(this)
          return false
        }

        tb.generateImpliedEndTags()
        if (!tb.currentElementIs(name)) {
          tb.errorState(this)
        }
        tb.popStackToClose(name)
      }
      break;
    case 'p':
      if (!tb.inButtonScope(name)) {
        tb.errorState(this)
        tb.processStartTag(name) // if no p to close, creates an empty <p></p>
        return tb.process(endTag)
      } else {
        tb.generateImpliedEndTags(name)
        if (!tb.currentElementIs(name)) {
          tb.errorState(this)
        }
        tb.popStackToClose(name)
      }
      break;
    case 'dd':
    case 'dt':
      if (!tb.inScope(name)) {
        tb.errorState(this)
        return false
      } else {
        tb.generateImpliedEndTags(name)
        if (!tb.currentElementIs(name)) {
          tb.errorState(this)
        }
        tb.popStackToClose(name)
      }
      break;
    case 'h1':
    case 'h2':
    case 'h3':
    case 'h4':
    case 'h5':
    case 'h6':
      if (!tb.inScope1(HtmlTreeBuilderStateConstants.Headings)) {
        tb.errorState(this)
        return false
      } else {
        tb.generateImpliedEndTags(name)
        if (!tb.currentElementIs(name)) {
          tb.errorState(this)
        }
        tb.popStackToCloses(...HtmlTreeBuilderStateConstants.Headings)
      }
      break
    case 'br':
      tb.errorState(this)
      tb.processStartTag('br')
      return false
    default:
    // todo - move rest to switch if desired
      if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InBodyEndAdoptionFormatters)) {
        return inBodyEndTagAdoption(t, tb);
      } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InBodyEndClosers)) {
        if (!tb.inScope(name)) {
          // nothing to close
          tb.errorState(this)
          return false
        } else {
          tb.generateImpliedEndTags()
          if (!tb.currentElementIs(name)) {
            tb.errorState(this)
          }
          tb.popStackToClose(name)
        }
      } else if (StringUtil.inSorted(name, HtmlTreeBuilderStateConstants.InBodyStartApplets)) {
        if (!tb.inScope('name')) {
          if (!tb.inScope(name)) {
            tb.errorState(this)
            return false
          }
          tb.generateImpliedEndTags()
          if (!tb.currentElementIs(name)) {
            tb.errorState(this)
          }
          tb.popStackToClose(name)
          tb.clearFormattingElementsToLastMarker()
        }
      } else {
        return anyOtherEndTag(t, tb)
      }
  }
  return true
}

function anyOtherEndTag(t: Token, tb: HtmlTreeBuilder): boolean {
  let name = t.asEndTag().normalName // case insensitive search - goal is to preserve output case, not for the parse to be case sensitive
  let stack = tb.getStack()

  // deviate from spec slightly to speed when super deeply nested
  let elFromStack = tb.getFromStack(name)
  if (elFromStack === undefined || elFromStack === null) {
    tb.errorState(this)
    return false
  }

  for (let pos = stack.length - 1; pos >= 0; pos--) {
    let node = stack.get(pos)
    if (node.normalName() === name) {
      tb.generateImpliedEndTags(name)
      if (!tb.currentElementIs(name)) {
        tb.errorState(this)
      }
      tb.popStackToClose(name)
      break
    } else {
      if (tb.isSpecial(node)) {
        tb.errorState(this)
        return false
      }
    }
  }
  return true
}

function inBodyEndTagAdoption(t: Token, tb: HtmlTreeBuilder): boolean {
  let endTag = t.asEndTag()
  let name = endTag.getNormalName()
  let stack = tb.getStack()
  let el: Element
  for (let i = 0; i < 8; i++) {
    let formatEl = tb.getActiveFormattingElement(name)
    if (formatEl == null) {
      return anyOtherEndTag(t, tb)
    } else if (!tb.onStack(formatEl)) {
      tb.errorState(this)
      tb.removeFromActiveFormattingElements(formatEl)
      return true
    } else if (!tb.inScope(formatEl.normalName())) {
      tb.errorState(this)
      return false
    } else if (tb.currentElement() != formatEl) {
      tb.errorState(this)
    }
    let furthestBlock: Element = null
    let commonAncestor: Element = null
    let seenFormattingElement = false
    // the spec doesn't limit to < 64, but in degenerate cases (9000+ stack depth) this prevents run-aways
    let stackSize = stack.length
    let bookmark = -1
    for (let si = 1; si < stackSize && si < 64; si++) {
      // TODO: this no longer matches the current spec at https://html.spec.whatwg.org/#adoption-agency-algorithm and should be updated
      el = stack.get(si)
      if (el == formatEl) {
        commonAncestor = stack.get(si - 1)
        seenFormattingElement = true
        // Let a bookmark note the position of the formatting element in the list of active formatting elements relative to the elements on either side of it in the list.
        bookmark = tb.positionOfElement(el)
      } else if (seenFormattingElement && tb.isSpecial(el)) {
        furthestBlock = el
        break
      }
    }
    if (furthestBlock == null) {
      tb.popStackToClose(formatEl.normalName())
      tb.removeFromActiveFormattingElements(formatEl)
      return true
    }

    let node = furthestBlock
    let lastNode = furthestBlock
    for (let j = 0; j < 3; j++) {
      if (tb.onStack(node)) {
        node = tb.aboveOnStack(node)
      }

      if (!tb.isInActiveFormattingElements(node)) { // note no bookmark check
        tb.removeFromStack(node)
        continue
      } else if (node == formatEl) {
        break
      }


      let replacement = new Element(tb.tagFor(node.nodeName(), ParseSettings.preserveCase), tb.getBaseUri())
      // case will follow the original node (so honours ParseSettings)
      tb.replaceActiveFormattingElement(node, replacement)
      tb.replaceOnStack(node, replacement)
      node = replacement

      if (lastNode == furthestBlock) {
        // move the aforementioned bookmark to be immediately after the new node in the list of active formatting elements.
        // not getting how this bookmark both straddles the element above, but is inbetween here...
        bookmark = tb.positionOfElement(node) + 1
      }
      if (lastNode.parent() != null) {
        lastNode.remove()
      }
      node.appendChild(lastNode)
      lastNode = node
    }
    if (commonAncestor != null) { // safety check, but would be an error if null
      if (StringUtil.inSorted(commonAncestor.normalName(), HtmlTreeBuilderStateConstants.InBodyEndTableFosters)) {
        if (lastNode.parent() != null) {
          lastNode.remove()
        }
        tb.insertInFosterParent(lastNode)
      } else {
        if (lastNode.parent() != null) {
          lastNode.remove()
        }
        commonAncestor.appendChild(lastNode)
      }
    }

    let adopter = new Element(formatEl.getTag(), tb.getBaseUri())
    adopter.attributes().addAll(formatEl.attributes())
    adopter.appendChildren(furthestBlock.childNodes())
    furthestBlock.appendChild(adopter)
    tb.removeFromActiveFormattingElements(formatEl)
    // insert the new element into the list of active formatting elements at the position of the aforementioned bookmark.
    tb.pushWithBookmark(adopter, bookmark)
    tb.removeFromStack(formatEl)
    tb.insertOnStackAfter(furthestBlock, adopter)
  }
  return true
}

function inTableAnythingElse(t: Token, tb: HtmlTreeBuilder): boolean {
  tb.errorState(this)
  tb.setFosterInserts(true)
  tb.process(t, HtmlTreeBuilderStates.InBody)
  tb.setFosterInserts(false)
  return true
}

function InColumnGroupAnythingElse(t: Token, tb: HtmlTreeBuilder): boolean {
  if (!tb.currentElementIs('colgroup')) {
    tb.errorState(this)
    return false
  }
  tb.pop()
  tb.transition(HtmlTreeBuilderStates.InTable)
  tb.process(t)
  return true
}